package jump

import (
	"io"
	"io/fs"
	"os"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"gitee.com/xuender/oils/base"
	"gitee.com/xuender/oils/logs"
	"gitee.com/xuender/oils/oss"
	"gitee.com/xuender/oils/u32"
	"github.com/hbollon/go-edlib"
	"github.com/pelletier/go-toml"
)

func NewJump(path string) *Jump {
	home := base.Panic1(os.UserHomeDir())

	if path[0] == '~' {
		path = home + path[1:]
	}

	path = base.Panic1(filepath.Abs(path))
	logs.Debugw("new", "path", path)
	stat, err := os.Stat(path)

	if os.IsNotExist(err) {
		// nolint
		base.Panic(os.MkdirAll(path, 0o700))
	}

	if err == nil && !stat.IsDir() {
		panic(ErrPathExist)
	}

	config := filepath.Join(path, "jump.toml")
	stat, err = os.Stat(config)

	if os.IsNotExist(err) {
		return &Jump{Config: config, Home: home, Paths: map[string]uint64{}}
	}

	if err == nil && stat.IsDir() {
		panic(ErrConfigIsDir)
	}

	configFile := base.Panic1(os.Open(config))
	defer configFile.Close()

	ret := &Jump{Paths: map[string]uint64{}}
	decoder := toml.NewDecoder(configFile)
	base.Panic(decoder.Decode(ret))

	ret.Config = config
	ret.Home = home

	if ret.Paths == nil {
		ret.Paths = map[string]uint64{}
	}

	return ret
}

func (p *Jump) Add(path string, num ...uint64) string {
	newPath, err := filepath.Abs(path)
	if err != nil {
		return ""
	}

	path = newPath

	if stat, err := os.Stat(path); os.IsNotExist(err) || !stat.IsDir() {
		return ""
	}

	if len(num) == 0 {
		p.Paths[path]++
	} else {
		p.Paths[path] += num[0]
	}

	logs.Debugw("add", "path", path, "count", p.Paths[path])

	return path
}

// nolint
var paths = []Path{}

func (p *Jump) Output(path string) {
	_, _ = io.WriteString(os.Stdout, p.Jump(path))
}

func (p *Jump) List() {
	p.list()
	sortPaths()

	for _, path := range paths {
		if path.Count == 0 {
			return
		}

		logs.Infow(path.Path, "count", path.Count)
	}
}

func (p *Jump) list() []Path {
	if p.Update == 0 {
		p.Init()
		p.History()
		p.Autojump()
	}

	for key, value := range p.Paths {
		if stat, err := os.Stat(key); os.IsNotExist(err) || !stat.IsDir() {
			delete(p.Paths, key)

			continue
		}

		paths = append(paths, Path{Path: key, Count: value})
	}

	return paths
}

func (p *Jump) Jump(path string) string {
	defer p.Save()

	p.list()

	if len(paths) == 0 {
		return path
	}
	// 当前目录加分
	p.Add(filepath.Dir(os.Args[0]))

	sortPaths()

	if ret := p.toFold(path); ret != "" {
		return ret
	}

	return p.toLevenshtein(path)
}

func sortPaths() {
	sort.Slice(paths, func(indexA, indexB int) bool {
		if paths[indexA].Level != paths[indexB].Level {
			return paths[indexA].Level < paths[indexB].Level
		}

		if paths[indexA].Count != paths[indexB].Count {
			return paths[indexA].Count > paths[indexB].Count
		}

		return len(paths[indexA].Path) < len(paths[indexB].Path)
	})
}

func (p *Jump) toLevenshtein(path string) string {
	for i, pat := range paths {
		paths[i].Level = edlib.LevenshteinDistance(filepath.Base(pat.Path), path)
	}

	sortPaths()

	for _, pat := range paths[:10] {
		logs.Debugw("level", "path", pat.Path, "level", pat.Level, "count", pat.Count)
	}

	p.Add(paths[0].Path)

	return paths[0].Path
}

func (p *Jump) toFold(path string) string {
	for _, pat := range paths {
		if strings.EqualFold(filepath.Base(pat.Path), path) {
			p.Add(pat.Path)

			return pat.Path
		}
	}

	return ""
}

func command(str string) string {
	slice := base.NewSlice([]rune(str)...)
	index := slice.Index(';')

	if index < 1 {
		return ""
	}

	slice = slice[index+1:]

	if slice.Indexs([]rune("cd ")) != 0 && slice[0] != '.' && slice[0] != '-' && slice[0] != '~' {
		return ""
	}

	return string(slice)
}

func (p *Jump) Autojump() {
	_ = oss.ReadLine(filepath.Join(p.Home, ".local", "share", "autojump", "autojump.txt"), func(str string) error {
		logs.Debug(str)
		strs := strings.Split(str, "\t")
		p.Add(strs[1], base.Pass1(base.ParseInteger[uint64](strs[0])))

		return nil
	})
}

func (p *Jump) History() {
	home := base.NewSlice("cd", "~", "cd ~")
	old, now := "", ""
	_ = oss.ReadLine(filepath.Join(p.Home, ".zsh_history"), func(str string) error {
		str = command(str)
		if str == "" {
			return nil
		}

		if home.Has(str) {
			old, now = now, p.Add(p.Home)

			return nil
		}

		str = strings.TrimPrefix(str, "cd ")

		if strings.HasPrefix(str, "~") {
			str = p.Home + str[1:]
		}

		if now != "" {
			switch str {
			case "-":
				old, now = now, p.Add(old)

				return nil
			case "..":
				old, now = now, p.Add(filepath.Dir(now))

				return nil
			case "...":
				old, now = now, p.Add(filepath.Dir(filepath.Dir(now)))

				return nil
			case "....":
				old, now = now, p.Add(filepath.Dir(filepath.Dir(filepath.Dir(now))))

				return nil
			}
		}

		now = p.Add(filepath.Join(now, str))

		return nil
	})
}

func (p *Jump) Init() {
	countHome := strings.Count(p.Home, string(os.PathSeparator))
	passes := []string{"/proc", "/tmp"}

	_ = filepath.WalkDir("/", func(path string, d fs.DirEntry, err error) error {
		if !d.IsDir() {
			return nil
		}

		if strings.Contains(path, "/.") {
			return filepath.SkipDir
		}

		p.Paths[path] = 0

		for _, pass := range passes {
			if strings.HasPrefix(path, pass) {
				return filepath.SkipDir
			}
		}

		count := strings.Count(path, string(os.PathSeparator))

		if strings.HasPrefix(path, p.Home) {
			if count-countHome > base.Two {
				return filepath.SkipDir
			}

			return nil
		}

		if count > 1 {
			return filepath.SkipDir
		}

		return nil
	})

	p.Update = u32.ParseTime(time.Now())
}

func (p *Jump) Save() {
	configFile := base.Panic1(os.Create(p.Config))
	defer configFile.Close()

	encoder := toml.NewEncoder(configFile)
	_ = encoder.Encode(p)
}
